package com.swingson.componentprocessor;

import com.swingson.SwingsonGui;
import com.swingson.SwingsonGuiBuilder;
import com.swingson.Utils;
import com.swingson.exceptions.SwingsonException;
import com.swingson.jsonparser.FieldValuePair;
import com.swingson.jsonparser.JsonElement;
import com.swingson.jsonparser.JsonObject;
import com.swingson.layout.processors.LayoutProcessor;
import com.swingson.properties.BasePropertyProcessor;
import java.awt.Container;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JComponent;

/**
 * @author sad
 */
public class JsonPropertiesProcessor {

    public void processProperties(JsonObject jsonElement, JComponent component, SwingsonGui gui, JComponent parent) {
        for (FieldValuePair entry : jsonElement.getElements()) {
            String key = entry.getName();
            JsonElement value = entry.getValue();
            if (SwingsonGuiBuilder.getComponentProcessorResolver().getProcessorOrNull(key) != null) {
                if (!value.isObject()) {
                    throw new SwingsonException(String.format("\"%s\" property should be JsonObject type", key));
                }
                JComponent child = processChild((JsonObject) value, component, gui, key);
                addComponentToParent(component, child);
            } else if (gui.getDefinedComponent(key) != null) {//this case for component that we defined in topComponent. It's label - field name. Example: "button1":{"text":"OK"}
                SwingsonGui.DefinedComponent defComponent = gui.getDefinedComponent(key);
                if (!value.isObject()) {
                    throw new SwingsonException("You should assign to property " + key + " only json object, because it is defined component");
                }
                processDefinedComponent(key, value.getAsObject(), component, defComponent.getComponent(), gui);
            } else if (SwingsonGuiBuilder.getPropertiesProcessorResolver().getNameClassProperty(key, component.getClass()) != null) {
                processProperty(key, value, component, gui);
            } else if (!tryToProcessPropertyAsClassMethodWriter(key, value, component, gui)) {
                throw new SwingsonException("Cannot process property '" + key + "' in component " + Utils.getComponentName(component));
            }
        }
    }

    private void addComponentToParent(Container parent, JComponent child) {
        LayoutProcessor layoutProcessor = SwingsonGuiBuilder.getLayoutProcessor(parent);
        BaseProcessor componentProcessor = SwingsonGuiBuilder.getComponentProcessorResolver().getProcessorForClass(parent.getClass());
        Object layoutProperty = layoutProcessor.getLayoutProperty(child);
        if (componentProcessor == null) {
            if (layoutProperty != null) {
                parent.add(child, layoutProperty);
            } else {
                parent.add(child);
            }
        } else {
            BaseProcessor childProcessor = SwingsonGuiBuilder.getComponentProcessorResolver().getProcessorForClass(child.getClass());
            BaseProcessor.CustomAddToParentStrategy strategy = childProcessor.getCustomAddToParentStategy();
            if (strategy == null) {
                componentProcessor.addCompomentToThis(parent, child, layoutProperty);
            } else {
                strategy.addCompomentToParent(parent, child, layoutProperty);
            }
        }
    }

    private JComponent processChild(JsonObject jsonElement, JComponent component, SwingsonGui gui, String key) {
        BaseProcessor processor = SwingsonGuiBuilder.getComponentProcessorResolver().getProcessorOrNull(key);
        JComponent child = processor.createComponent(jsonElement, component, gui);
        processor.processComponent(jsonElement, child, component, gui);
        return child;
    }

    private void processDefinedComponent(String key, JsonObject value, JComponent parent, JComponent component, SwingsonGui gui) {
        BaseProcessor propProcessor = SwingsonGuiBuilder.getComponentProcessorResolver().getProcessorForClass(component.getClass());
        propProcessor.processComponent(value, component, parent, gui);
        if (component.getParent() == null) {
            addComponentToParent(parent, component);
        } else if (component.getParent() != parent) {
//            throw new SwingsonException("You try to process defined component " + key + " in wrong place. His parent in json is not real parent");
        }
    }

    private void processProperty(String key, JsonElement value, JComponent component, SwingsonGui gui) {
        BasePropertyProcessor processor = SwingsonGuiBuilder.getPropertiesProcessorResolver().getNameClassProperty(key, component.getClass());
        processor.processProperty(value, component, gui, key);
    }

    private boolean tryToProcessPropertyAsClassMethodWriter(String key, JsonElement value, JComponent component, SwingsonGui gui) {
        Method method = getWriterMethod(component.getClass(), key);
        if (method == null) {
            return false;
        }

        if (method.getParameterTypes().length != 1) {
            throw new SwingsonException("You try to set property " + key + " in class " + component.getClass() + ", but found setter method should has only 1 argument, but found " + method.getParameterTypes().length);
        }

        BasePropertyProcessor propProcessor = SwingsonGuiBuilder.getPropertiesProcessorResolver().getTypeProcessingPropertyProcessor(method.getParameterTypes()[0]);
        if (propProcessor == null) {
            throw new RuntimeException("You try to set property " + key + " with type " + method.getParameterTypes()[0].getSimpleName() + " but there is no such properties processor");
        }

        Object result = propProcessor.processProperty(value, component, gui, key);
        try {
            method.invoke(component, result);
            return true;
        } catch (IllegalArgumentException ex) {
            throw new SwingsonException("error while executing method " + method.getName() + " in class " + component.getClass().getSimpleName() + " while set property " + key + " the value " + result);
        } catch (Exception ex) {
            throw new SwingsonException("error while executing method " + method.getName() + " in class " + component.getClass().getSimpleName() + " while set property " + key + " the value " + result);
        }
    }

    /*private Field getField(Class clazz, String fieldName) {
     String id = clazz.getSimpleName() + "|" + fieldName;
     if (fieldAndClassNameToFieldObjectCache.containsKey(id)) {
     return fieldAndClassNameToFieldObjectCache.get(id);
     }
     while (clazz != null) {
     try {
     Field field = clazz.getDeclaredField(fieldName);
     field.setAccessible(true);
     fieldAndClassNameToFieldObjectCache.put(id, field);
     return field;
     } catch (NoSuchFieldException | SecurityException ex) {
     }
     clazz = clazz.getSuperclass();
     }
     return null;
     }*/
    private Method getWriterMethod(Class clazz, String property) {
        Method method = Utils.getMethod(clazz, "set" + Utils.makeFirstLetterBig(property));
        return method;
    }

    private final Map<String, Field> fieldAndClassNameToFieldObjectCache = new HashMap<String, Field>();

}
